﻿// (c) Norbert Huffschmid
// This source is subject to the Microsoft Public License (Ms-PL).
// Please see http://go.microsoft.com/fwlink/?LinkID=131993 for details.

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace SpinDotters.Core
{
    public class CylinderPanel : Panel
    {
        /// <summary>
        /// Event fired when spin angle has changed
        /// </summary>
        public event EventHandler SpinAngleChanged;


        private double desiredWidth;
        private double desiredHeight;
        protected double wheelDiameter;
        private double viewAngle;


        /// <summary>
        /// SpinAngle dependency property.
        /// </summary>
        public static readonly DependencyProperty SpinAngleProperty =
            DependencyProperty.Register(
                "SpinAngle",
                typeof(double),
                typeof(CylinderPanel),
                new PropertyMetadata(0.0, new PropertyChangedCallback(OnSpinAngleChanged))
            );

        /// <summary>
        /// Get the current spin angle
        /// </summary>
        public double SpinAngle
        {
            get { return (double)GetValue(SpinAngleProperty); }
            set { SetValue(SpinAngleProperty, value); }
        }

        /// <summary>
        /// Orientation dependency property.
        /// </summary>
        public static readonly DependencyProperty OrientationProperty =
            DependencyProperty.Register(
                "Orientation",
                typeof(Orientation),
                typeof(CylinderPanel),
                new PropertyMetadata(UpDownBase.ORIENTATION_DEFAULT));

        /// <summary>
        /// Gets or sets the spin orientation.
        /// </summary>
        public Orientation Orientation
        {
            get { return (Orientation)GetValue(OrientationProperty); }
            set { SetValue(OrientationProperty, value); }
        }

        /// <summary>
        /// Expanded dependency property.
        /// </summary>
        public static readonly DependencyProperty ExpandedProperty =
            DependencyProperty.Register(
                "Expanded",
                typeof(bool),
                typeof(CylinderPanel),
                new PropertyMetadata(false));

        /// <summary>
        /// Gets or sets the expanded flag.
        /// </summary>
        public bool Expanded
        {
            get { return (bool)GetValue(ExpandedProperty); }
            set
            {
                SetValue(ExpandedProperty, value);

                this.InvalidateMeasure();
            }
        }

        /// <summary>
        /// Get the index of the currently visible child
        /// </summary>
        public int CurrentIndex
        {
            get
            {
                int index = (int)(0.5 + (NormalizeAngle(this.SpinAngle) / 360.0 * this.Children.Count));

                if (index == this.Children.Count)
                    index = 0;

                return index;
            }
        }


        /// <summary>
        /// Constructor
        /// </summary>
        public CylinderPanel()
        {
            this.SizeChanged += new SizeChangedEventHandler(CylinderPanel_SizeChanged);
        }


        protected virtual void OnSpinAngleChanged()
        {
            if (SpinAngleChanged != null)
                SpinAngleChanged(this, EventArgs.Empty);
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            this.wheelDiameter = 100;
            this.viewAngle = 90;
            this.desiredHeight = 0;


            if (this.Expanded)
            {
                foreach (UIElement child in this.Children)
                {
                    child.Visibility = Visibility.Visible;

                    child.Measure(availableSize);

                    if (child.DesiredSize.Width > this.desiredWidth)
                        this.desiredWidth = child.DesiredSize.Width;

                    this.desiredHeight += child.DesiredSize.Height;
                }
            }
            else
            {
                if (this.Orientation == Orientation.Vertical)
                {
                    foreach (UIElement child in this.Children)
                    {
                        child.Measure(availableSize);

                        if (child.DesiredSize.Width > this.desiredWidth)
                            this.desiredWidth = child.DesiredSize.Width;

                        if (child.DesiredSize.Height > this.desiredHeight)
                            this.desiredHeight = child.DesiredSize.Height;
                    }

                    // required e.g. in case of orientation change
                    if (desiredWidth > availableSize.Width)
                        desiredWidth = availableSize.Width;

                    this.wheelDiameter = this.desiredHeight * this.Children.Count / Math.PI;

                    this.viewAngle = Math.Asin(1.5 * availableSize.Height / this.wheelDiameter) * 180.0 / Math.PI;
                    if (Double.IsNaN(this.viewAngle))
                        this.viewAngle = 90;
                }
                else
                {
                    foreach (UIElement child in this.Children)
                    {
                        child.Measure(availableSize);

                        if (child.DesiredSize.Width > this.desiredWidth)
                            this.desiredWidth = child.DesiredSize.Width;

                        if (child.DesiredSize.Height > this.desiredHeight)
                            this.desiredHeight = child.DesiredSize.Height;
                    }

                    if (desiredHeight > availableSize.Height)
                        desiredHeight = availableSize.Height;

                    this.wheelDiameter = this.desiredWidth * this.Children.Count / Math.PI;

                    this.viewAngle = Math.Asin(1.5 * availableSize.Width / this.wheelDiameter) * 180.0 / Math.PI;
                    if (Double.IsNaN(this.viewAngle))
                        this.viewAngle = 90;
                }
            }

            return new Size(desiredWidth, desiredHeight);
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            if (this.Expanded)
            {
                double y = 0;

                foreach (UIElement item in this.Children)
                {
                    item.Arrange(new Rect(0, y, finalSize.Width, item.DesiredSize.Height));
                    item.RenderTransform = null;

                    y += item.DesiredSize.Height;
                }
            }
            else
            {
                if (this.Children.Count > 0)
                {
                    // determine spin angle between items
                    double stepAngle = 360.0 / this.Children.Count;

                    if (this.Orientation == Orientation.Vertical)
                    {
                        if (this.wheelDiameter < finalSize.Height)
                            this.wheelDiameter = finalSize.Height;
                    }
                    else
                    {
                        if (this.wheelDiameter < finalSize.Width)
                            this.wheelDiameter = finalSize.Width;
                    }

                    for (int i = 0; i < this.Children.Count; i++)
                    {
                        UIElement item = this.Children[i];

                        // determine delta angle related to given index
                        double deltaAngle = NormalizeAngle(this.SpinAngle) - i * stepAngle;
                        if (deltaAngle >= 180)
                            deltaAngle -= 360;
                        if (deltaAngle < -180)
                            deltaAngle += 360;

                        // convert into radian measure
                        double deltaAngleRad = deltaAngle * Math.PI / 180.0;

                        // check for visibility
                        if (Math.Abs(deltaAngle) < this.viewAngle)
                        {
                            if ((item.Visibility == Visibility.Collapsed) && (item is TextBlock))
                                // work around silverlight fontsize measurement bug (???)
                                ((TextBlock)item).FontSize += 0;

                            item.Visibility = Visibility.Visible;

                            item.Arrange(new Rect(0, 0, finalSize.Width, finalSize.Height));

                            ScaleTransform scaleTransform;
                            TranslateTransform translateTransform;

                            if (!(item.RenderTransform is TransformGroup))
                            {
                                // prepare transformations
                                item.RenderTransform = new TransformGroup();
                                item.RenderTransformOrigin = new Point(0.5, 0.5);
                                scaleTransform = new ScaleTransform();
                                ((TransformGroup)item.RenderTransform).Children.Add(scaleTransform);
                                translateTransform = new TranslateTransform();
                                ((TransformGroup)item.RenderTransform).Children.Add(translateTransform);
                            }
                            else
                            {
                                // transformations have already been prepared
                                scaleTransform = (ScaleTransform)((TransformGroup)item.RenderTransform).Children[0];
                                translateTransform = (TranslateTransform)((TransformGroup)item.RenderTransform).Children[1];
                            }

                            if (this.Orientation == Orientation.Vertical)
                            {
                                scaleTransform.ScaleX = 1;
                                scaleTransform.ScaleY = Math.Cos(deltaAngleRad);
                                translateTransform.X = 0;
                                translateTransform.Y = -0.5 * this.wheelDiameter * Math.Sin(deltaAngleRad);
                            }
                            else
                            {
                                scaleTransform.ScaleX = Math.Cos(deltaAngleRad);
                                scaleTransform.ScaleY = 1;
                                translateTransform.X = -0.5 * this.wheelDiameter * Math.Sin(deltaAngleRad);
                                translateTransform.Y = 0;
                            }
                        }
                        else
                            item.Visibility = Visibility.Collapsed;
                    }
                }
            }
             
            return finalSize;
        }

        protected static double NormalizeAngle(double angle)
        {
            while (angle >= 360)
                angle -= 360;
            while (angle < 0)
                angle += 360;

            return angle;
        }


        private void CylinderPanel_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            // update clip region
            RectangleGeometry rg = new RectangleGeometry();
            rg.Rect = new Rect(0, 0, this.ActualWidth, this.ActualHeight);
            this.Clip = rg;
        }

        private static void OnSpinAngleChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e)
        {
            CylinderPanel cylinderPanel = (CylinderPanel)dependencyObject;

            if (cylinderPanel != null)
            {
                cylinderPanel.InvalidateArrange();

                // notify listeners
                cylinderPanel.OnSpinAngleChanged();
            }
        }
    }
}
